{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "# **Private and protected attributes** in Python DataClasses.\n",
        "\n",
        "Python, by design, does not have strict enforcement of “private” or “protected” visibility like some other languages (e.g. Java or C++). Instead, Python developers rely on **naming conventions** (single underscore `_` or double underscore `__`) and sometimes **property getters/setters** to indicate or simulate restricted access.\n",
        "\n",
        "We will walk through **five** illustrative examples, ranging from basic conventions to more advanced usage with data classes.\n",
        "\n",
        "---\n"
      ],
      "metadata": {
        "id": "bqWEbIAISSDD"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 1. Python’s Visibility Conventions: A Refresher\n",
        "\n",
        "Before we jump into data classes, let’s recall how Python typically indicates private or protected attributes:\n",
        "\n",
        "- **Public Attributes**: No leading underscores. Accessible everywhere (e.g. `myobject.value`).\n",
        "- **Protected Attributes** (by convention): Single leading underscore (e.g. `_secret`).  \n",
        "  - This *suggests* other developers should not use it externally, but nothing prevents them from doing so.\n",
        "- **Private Attributes** (by name mangling): Double leading underscore (e.g. `__very_secret`).  \n",
        "  - **Name mangling**: Python automatically renames `__very_secret` to `_<ClassName>__very_secret`. This helps avoid accidental overrides in subclasses.  \n",
        "  - This is still *not truly private*—you can technically access it by referencing its mangled name, but it’s a strong indicator that it’s internal.\n",
        "\n",
        "---"
      ],
      "metadata": {
        "id": "ByMf1u3vSxT3"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 2. Basic DataClass with Underscore Conventions\n",
        "\n",
        "### Example 1: Single Underscore and Double Underscore Fields\n",
        "\n",
        "Let’s start with a simple data class containing three attributes: public, protected, and private."
      ],
      "metadata": {
        "id": "LtKmqGhbS4q3"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from dataclasses import dataclass\n",
        "\n",
        "@dataclass\n",
        "class Person:\n",
        "    name: str                   # Public\n",
        "    _age: int                   # Protected (convention)\n",
        "    __bank_account_balance: int # Private (name mangling)"
      ],
      "metadata": {
        "id": "3QIrk0RKS8f4"
      },
      "execution_count": 13,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "**Explanation:**\n",
        "1. **Public Field**: `name`. In normal usage, anyone can do `person.name`.\n",
        "2. **Protected Field**: `_age`. By convention, this suggests “internal use.” Another developer *can* do `person._age`, but it’s usually frowned upon unless absolutely necessary.\n",
        "3. **Private Field**: `__bank_account_balance`. Python will automatically rename this to `_Person__bank_account_balance` internally, providing some limited protection from accidental access.\n"
      ],
      "metadata": {
        "id": "SfIM5wXUVO24"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "In Python, attributes that start with two underscores are \"mangled\" to include the class name\n",
        "(e.g. __bank_account_balance becomes _Person__bank_account_balance internally).\n",
        "\n",
        "This means that when you try to pass __bank_account_balance=100 as a keyword argument,\n",
        "it doesn't match the parameter name generated by the dataclass's __init__."
      ],
      "metadata": {
        "id": "oQs76_4AU3bu"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# ERROR CODE\n",
        "# junaid = Person(name=\"Junaid\", _age=20, __bank_account_balance=100)\n",
        "# print(junaid)\n",
        "\n",
        "# BAD METHOD\n",
        "# **Check name mangling**:\n",
        "bob = Person(name=\"Bob\", _age=30, _Person__bank_account_balance=1000)\n",
        "\n",
        "print(bob.name)      # \"Bob\"\n",
        "print(bob._age)      # \"30\" (still directly accessible, just discouraged)\n",
        "# print(bob.__bank_account_balance)  # AttributeError\n",
        "print(bob._Person__bank_account_balance)  # 1000, \"private\" but hackable\n",
        "\n",
        "# - Attempting `bob.__bank_account_balance` raises `AttributeError`.\n",
        "# - You *can* access it via the mangled name `_Person__bank_account_balance`.\n",
        "# This is **Example #1**: a straightforward demonstration of naming conventions in a data class."
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "p2ICYbKuTm8-",
        "outputId": "8ba3d1a0-4cdb-444f-bd41-e499d5df426c"
      },
      "execution_count": 34,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Bob\n",
            "30\n",
            "1000\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "from dataclasses import dataclass, field\n",
        "\n",
        "@dataclass\n",
        "class TruePerson:\n",
        "    name: str                   # Public\n",
        "    _age: int                   # Protected (convention)\n",
        "    __bank_account_balance: int = field(default=0)\n",
        "\n",
        "    def set_bank_balance(self, balance: int):\n",
        "        if balance < 0:\n",
        "            raise ValueError(\"Balance cannot be negative.\")\n",
        "        self.__bank_account_balance = balance\n",
        "\n",
        "    def get_account_balance(self) -> int:\n",
        "        return self.__bank_account_balance\n",
        "\n",
        "junaid = TruePerson(name=\"Junaid\", _age=20)\n",
        "print(junaid)\n",
        "\n",
        "junaid.set_bank_balance(100)\n",
        "print(junaid)\n",
        "\n",
        "junaid.get_account_balance()\n"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "uGVv6iAjTuZa",
        "outputId": "391a7e8a-5d88-4562-f5e0-a99ce6efd383"
      },
      "execution_count": 37,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "TruePerson(name='Junaid', _age=20, _TruePerson__bank_account_balance=0)\n",
            "TruePerson(name='Junaid', _age=20, _TruePerson__bank_account_balance=100)\n"
          ]
        },
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "100"
            ]
          },
          "metadata": {},
          "execution_count": 37
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# 3. Understanding Name Mangling and Its Purpose\n",
        "\n",
        "### Example 2: Inheritance and Name Mangling\n",
        "\n",
        "One major reason for double underscore name mangling is to **avoid attribute collisions** in subclasses. Let’s see how that works:"
      ],
      "metadata": {
        "id": "kY6cD7ZqVSid"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "@dataclass\n",
        "class Base:\n",
        "    __private_var: str = \"Base Private\"\n",
        "\n",
        "    def reveal(self):\n",
        "        return self.__private_var\n",
        "\n",
        "@dataclass\n",
        "class Derived(Base):\n",
        "    __private_var: str = \"Derived Private\"\n",
        "\n",
        "    def reveal_derived(self):\n",
        "        return self.__private_var\n",
        "\n",
        "base = Base()\n",
        "derived = Derived()\n",
        "\n",
        "print(base.reveal())           # \"Base Private\"\n",
        "print(derived.reveal())        # \"Base Private\" -> calls Base.reveal()\n",
        "print(derived.reveal_derived())# \"Derived Private\""
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "itgbzlhDVVkj",
        "outputId": "edc0ec2e-d0cc-41a5-ac55-74709a6a2c31"
      },
      "execution_count": 39,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Base Private\n",
            "Base Private\n",
            "Derived Private\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "**Explanation**:\n",
        "1. `Base` defines a double underscore attribute `__private_var`.  \n",
        "2. `Derived` also defines a double underscore attribute named the *same* but is internally renamed to `_Derived__private_var`.  \n",
        "3. When `derived.reveal()` is called, it uses `Base.reveal()`, which returns `_Base__private_var` → `\"Base Private\"`.  \n",
        "4. When `derived.reveal_derived()` is called, it returns `_Derived__private_var` → `\"Derived Private\"`.\n",
        "\n",
        "This shows how name mangling keeps attributes with the same “private” name in different classes from clashing. If we had used a single underscore `_private_var`, the subclass attribute would have overridden or conflicted with the base class attribute.\n",
        "\n",
        "This is **Example #2**: illustrating **why** double underscores might be preferable when dealing with inheritance.\n",
        "\n",
        "---\n",
        "\n",
        "# 4. Using Properties to “Protect” Data in DataClasses\n",
        "\n",
        "### Example 3: A DataClass with a Private Field + Property\n",
        "\n",
        "Although underscore conventions and name mangling provide a hint, we often want to control *how* the field is accessed or modified. **Properties** can handle that:"
      ],
      "metadata": {
        "id": "DLUFSoKqViJX"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from dataclasses import dataclass, field\n",
        "\n",
        "@dataclass\n",
        "class Employee:\n",
        "    name: str\n",
        "    __salary: float = field(repr=False, default=0)  # Not shown in __repr__\n",
        "\n",
        "    @property\n",
        "    def salary(self) -> float:\n",
        "        \"\"\"Read-only property that returns the private salary.\"\"\"\n",
        "        return self.__salary\n",
        "\n",
        "    @salary.setter\n",
        "    def salary(self, new_salary: float):\n",
        "        \"\"\"Setter that validates the new salary.\"\"\"\n",
        "        if new_salary < 0:\n",
        "            raise ValueError(\"Salary cannot be negative.\")\n",
        "        self.__salary = new_salary\n",
        "\n",
        "emp = Employee(name=\"Alice\")\n",
        "print(emp)          # Employee(name='Alice')\n",
        "print(emp.salary)   # 0\n",
        "emp.salary = 55000\n",
        "print(emp.salary)   # 55000\n",
        "# emp.__salary   # Will raise AttributeError"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "YfhEcPy1VkzQ",
        "outputId": "23b01e6c-6537-43bd-d01b-1552969c2f65"
      },
      "execution_count": 42,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Employee(name='Alice')\n",
            "0\n",
            "55000\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "**Key points**:\n",
        "1. We store `__salary` as a “private” field.  \n",
        "2. We create a `@property` named `salary` to **get** (`return self.__salary`) and **set** (`self.__salary = new_salary`) the salary value.  \n",
        "3. Notice the use of `repr=False` in `field(...)`: prevents the private field from appearing in the auto-generated `repr`.\n",
        "\n",
        "This is **Example #3**, showing how you can combine data classes, name mangling, and properties for a more idiomatic “private” approach.\n",
        "\n",
        "---\n",
        "\n",
        "# 5. Combining Protected Fields with Slots or Additional Methods\n",
        "\n",
        "### Example 4: A DataClass with `_protected` Attributes and Advanced Usage\n",
        "\n",
        "Let’s create a data class that uses `_protected_value` to indicate a field that’s not meant for external usage, but we’ll add methods to read/write it carefully."
      ],
      "metadata": {
        "id": "nuwKFkMnVvIP"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from dataclasses import dataclass\n",
        "\n",
        "@dataclass\n",
        "class Settings:\n",
        "    database_url: str\n",
        "    _api_token: str    # Protected by convention\n",
        "\n",
        "    def get_api_token(self) -> str:\n",
        "        \"\"\"A method to safely retrieve the protected token.\"\"\"\n",
        "        # Possibly perform logging or checks here\n",
        "        return self._api_token\n",
        "\n",
        "    def set_api_token(self, token: str):\n",
        "        \"\"\"A method to safely update the protected token.\"\"\"\n",
        "        if not token.startswith(\"tok_\"):\n",
        "            raise ValueError(\"API token must start with 'tok_'.\")\n",
        "        self._api_token = token\n",
        "\n",
        "config = Settings(database_url=\"postgres://localhost\", _api_token=\"tok_ABC123\")\n",
        "print(config.get_api_token())  # \"tok_ABC123\"\n",
        "\n",
        "# Even though it's \"protected\", direct access is possible in Python:\n",
        "config._api_token = \"tok_Override\"  # Not recommended, but won't crash\n",
        "print(config.get_api_token())       # \"tok_Override\"\n",
        "\n",
        "# Proper usage via setter\n",
        "config.set_api_token(\"tok_NEW456\")\n",
        "print(config.get_api_token())  # \"tok_NEW456\""
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "2BOKnYFGVyWw",
        "outputId": "f00a7c94-3e03-4cb1-f17c-7455efb783af"
      },
      "execution_count": 43,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "tok_ABC123\n",
            "tok_Override\n",
            "tok_NEW456\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "**Notes**:\n",
        "- The single underscore `_api_token` clarifies “internal usage only.”  \n",
        "- We provide dedicated methods: `get_api_token()` and `set_api_token()` with minimal logic.  \n",
        "- In reality, a developer *can* still do `config._api_token = \"raw_token\"`. This is Python’s “we’re all consenting adults” philosophy, but the underscore is a **hint** not to do that.\n",
        "\n",
        "This is **Example #4**, showing a “protected” attribute with accessor methods.\n",
        "\n",
        "---\n",
        "\n",
        "# 6. Chaining It All Together in a More Complex DataClass\n",
        "\n",
        "### Example 5: Mixed Access Levels in One Class\n",
        "\n",
        "Let’s build a real-world(ish) scenario with an e-commerce `Order` class containing:\n",
        "- **Public** `order_id`\n",
        "- **Protected** `_discount_code`\n",
        "- **Private** `__internal_tax_rate`\n",
        "- Property-based logic to compute final price\n"
      ],
      "metadata": {
        "id": "lFiys0GUSjyo"
      }
    },
    {
      "cell_type": "code",
      "execution_count": 47,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "uao6yzmzRJR9",
        "outputId": "175cf26c-1030-4613-df59-8abfd400b7b0"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "110.0\n",
            "55.0\n",
            "0.1\n"
          ]
        }
      ],
      "source": [
        "from dataclasses import dataclass\n",
        "\n",
        "@dataclass\n",
        "class Order:\n",
        "    order_id: int\n",
        "    base_price: float\n",
        "    _discount_code: str = \"\"         # Protected\n",
        "    __internal_tax_rate: float = 0.1 # Private\n",
        "\n",
        "    # The @property decorator turns the total_price method into a computed attribute.\n",
        "    # This means that when you access order.total_price, Python automatically calls the method\n",
        "    @property\n",
        "    def total_price(self) -> float:\n",
        "        \"\"\"\n",
        "        The final price factoring in discount (if any) and tax.\n",
        "        \"\"\"\n",
        "        discounted = self.base_price\n",
        "        if self._discount_code == \"BLACKFRIDAY\":\n",
        "            discounted *= 0.5  # 50% off\n",
        "\n",
        "        # Access the name-mangled attribute for final calculation\n",
        "        return discounted + (discounted * self.__internal_tax_rate)\n",
        "\n",
        "    def set_discount_code(self, code: str):\n",
        "        \"\"\"Method to safely set a discount code.\"\"\"\n",
        "        # We can implement checks, e.g. only certain codes are valid\n",
        "        self._discount_code = code\n",
        "\n",
        "# Usage\n",
        "order = Order(order_id=101, base_price=100.0)\n",
        "print(order.total_price)  # 110.0 (10% tax on 100)\n",
        "\n",
        "order.set_discount_code(\"BLACKFRIDAY\")\n",
        "print(order.total_price)  # 55.0 (50% discount => 50, plus 10% tax => 5 => 55 total)\n",
        "\n",
        "# Attempting direct private access:\n",
        "# print(order.__internal_tax_rate)  # AttributeError\n",
        "# But we can do:\n",
        "print(order._Order__internal_tax_rate)  # 0.1, not recommended to do so externally!"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "**Explanation**:\n",
        "1. **Public**: `order_id` and `base_price`.  \n",
        "2. **Protected**: `_discount_code`, indicating it is an internal detail about discounts that we set via `set_discount_code()`.  \n",
        "3. **Private**: `__internal_tax_rate`, used only inside class methods.  \n",
        "4. **Property**: `total_price` uses the private tax rate to compute the final price after any discount.  \n",
        "\n",
        "This is **Example #5**, combining multiple levels of attribute visibility for a more “production-like” scenario.\n",
        "\n",
        "---\n",
        "\n",
        "## Practical Takeaways\n",
        "\n",
        "1. **Python does not enforce private or protected**:  \n",
        "   - Double-underscore (“private”) attributes are just name-mangled but still accessible if someone tries hard enough.  \n",
        "   - Single underscore (“protected”) is purely a convention to discourage external use.\n",
        "2. **Use Properties** to manage “get” and “set” logic:  \n",
        "   - This is often the best practice to protect or validate data in a data class (or any class in Python).\n",
        "3. **Name Mangling** primarily helps avoid conflicts in inheritance or accidental overrides.  \n",
        "4. **When in Doubt, Keep It Simple**: If you’re creating a library or large-scale system, use underscore conventions + docstrings to communicate intent. Resist overusing double underscores unless you have a specific naming-conflict scenario.\n",
        "\n",
        "---\n",
        "\n",
        "# Final Summary\n",
        "\n",
        "**Private & Protected in Python** are **conventions** rather than rigid rules. Data classes add no special enforcement for private or protected attributes but integrate seamlessly with Python’s underscores and name-mangling. By understanding these conventions, leveraging properties, and writing clear docstrings, you can create robust, maintainable, and “semi-private” data structures that convey your intentions to other developers.\n",
        "\n",
        "Feel free to mix and match these patterns, but remember that Python is a language that emphasizes readability and developer cooperation—so always document clearly which fields are meant for external use and which are internal only."
      ],
      "metadata": {
        "id": "BlxcpOEJSnsX"
      }
    }
  ]
}